1 /* 2 Copyright: Marcelo S. N. Mancini (Hipreme|MrcSnm), 2018 - 2021 3 License: [https://creativecommons.org/licenses/by/4.0/|CC BY-4.0 License]. 4 Authors: Marcelo S. N. Mancini 5 6 Copyright Marcelo S. N. Mancini 2018 - 2021. 7 Distributed under the CC BY-4.0 License. 8 (See accompanying file LICENSE.txt or copy at 9 https://creativecommons.org/licenses/by/4.0/ 10 */ 11 module hip.audio.clip; 12 import hip.util.path : baseName; 13 import hip.error.handler; 14 import hip.audio_decoding.audio; 15 import hip.audio; 16 import hip.audio.audiosource; 17 import hip.api.data.asset; 18 public import hip.api.audio.audioclip; 19 20 21 union HipAudioBuffer 22 { 23 import hip.config.audio; 24 static if(HasOpenAL) 25 { 26 import bindbc.openal; 27 ALuint al; 28 } 29 static if(HasOpenSLES) 30 { 31 import opensles.sles; 32 import hip.audio.backend.sles; 33 SLIBuffer* sles; 34 } 35 static if(HasXAudio2) 36 { 37 import directx.xaudio2; 38 XAUDIO2_BUFFER* xaudio; 39 } 40 static if(HasWebAudio) 41 { 42 import hip.audio.backend.webaudio.clip; 43 size_t webaudio; 44 } 45 static if(HasAVAudioEngine) 46 { 47 import hip.audio.backend.avaudio.clip; 48 AVAudioPCMBuffer avaudio; 49 } 50 } 51 52 struct HipAudioBufferWrapper 53 { 54 HipAudioBuffer buffer; 55 bool isAvailable; 56 } 57 58 59 60 /** 61 * Wraps a decoder onto it. Basically an easier interface with some more controls 62 * that would be needed inside specific APIs. 63 * 64 * AudioClip flow basically consists in: 65 * 1. Initialize the audio clip with the current decoder. 66 * 2. Call `.load`, which calls `.decode` 67 * 3. HipAudioSource calls `.setClip`, which should call `clip.getBuffer`, which gets the buffer 68 * wrapped by the current implementation `createBuffer`, and then the buffer is enqueued. 69 */ 70 public abstract class HipAudioClip : HipAsset, IHipAudioClip 71 { 72 IHipAudioDecoder decoder; 73 ///Unused for non streamed. It is the binary loaded from a file which will be decoded 74 ubyte[] dataToDecode; 75 ///Unused for non streamed. Where the user will get its audio decoded. 76 ubyte[] outBuffer; 77 ///Unused for non streamed 78 uint chunkSize; 79 80 81 HipAudioClipHint hint; 82 83 /** 84 * Buffers recycled from HipAudioSource. 85 * 86 * When source notifies that the buffer is free, it is added to 87 * that array. When getBuffer is called, it could send one 88 * of those recycleds. 89 */ 90 private HipAudioBufferWrapper[] buffersToRecycle; 91 private HipAudioBufferWrapper[] buffersCreated; 92 93 size_t totalDecoded = 0; 94 95 HipAudioType type; 96 HipAudioEncoding encoding; 97 bool isStreamed = false; 98 string fileName; 99 100 101 102 ///Event method called when the stream is updated 103 protected abstract void onUpdateStream(ubyte[] data, uint decodedSize); 104 /** 105 * Always alocates a pointer to the buffer data. So, after getting its content. Send it to the 106 * recyclable buffers 107 */ 108 protected abstract HipAudioBufferWrapper createBuffer(ubyte[] data); 109 protected abstract void destroyBuffer(HipAudioBuffer* buffer); 110 111 /** The buffer is actually any kind of external API buffer, it is the buffer contained in 112 * HipAudioBufferWrapper. 113 * 114 * OpenAL: `int` containing the buffer ID 115 * OpenSL ES: `SLIBuffer` 116 * XAudio2: To be thought? 117 */ 118 public abstract void setBufferData(HipAudioBuffer* buffer, ubyte[] data, uint size); 119 120 final immutable(HipAudioClipHint)* getHint(){return cast(immutable)&hint;} 121 122 this() 123 { 124 super("HipAudioClip"); 125 _typeID = assetTypeID!HipAudioClip(); 126 } 127 128 this(IHipAudioDecoder decoder, HipAudioClipHint hint){this(); this.decoder = decoder; this.hint = hint;} 129 this(IHipAudioDecoder decoder, HipAudioClipHint hint, uint chunkSize) 130 in(chunkSize > 0, "Chunk must be greater than 0") 131 { 132 this(decoder, hint); 133 this.chunkSize = chunkSize; 134 outBuffer = new ubyte[chunkSize]; 135 ErrorHandler.assertExit(outBuffer != null, "Out of memory"); 136 } 137 /** 138 * Should implement the specific loading here 139 */ 140 public bool loadFromMemory(in ubyte[] data, HipAudioEncoding encoding, HipAudioType type, 141 void delegate(in ubyte[]) onSuccess, void delegate() onFailure) 142 { 143 this.type = type; 144 this.isStreamed = false; 145 return decoder.loadData(data, encoding, type, hint, onSuccess, onFailure); 146 } 147 /** 148 * Decodes a bit more of the current buffer 149 */ 150 public final uint updateStream() 151 { 152 ErrorHandler.assertExit(chunkSize > 0, "Can't update stream with 0 sized buffer."); 153 uint dec = decoder.updateDecoding(outBuffer); 154 totalDecoded+= dec; 155 onUpdateStream(outBuffer, dec); 156 return dec; 157 } 158 package final HipAudioBufferWrapper* findBuffer(HipAudioBuffer buf) 159 { 160 foreach(ref b; buffersCreated) 161 if(b.buffer == buf) 162 return &b; 163 return null; 164 } 165 166 /** 167 * Attempts to get a buffer from the buffer recycler. 168 * Used for when loadStreamed must set a buffer available 169 */ 170 public final HipAudioBuffer pollFreeBuffer() 171 { 172 if(buffersToRecycle.length > 0) 173 { 174 HipAudioBufferWrapper* w = &(buffersToRecycle[$ - 1]); 175 buffersToRecycle.length--; 176 w.isAvailable = false; 177 return w.buffer; 178 } 179 return HipAudioBuffer.init; 180 } 181 182 public final HipAudioBuffer getBuffer(ubyte[] data, uint size) 183 { 184 HipAudioBuffer ret; 185 if((ret = pollFreeBuffer()) != HipAudioBuffer.init) 186 { 187 setBufferData(&ret, data, size); 188 return ret; 189 } 190 HipAudioBufferWrapper w = createBuffer(data); 191 setBufferData(&w.buffer, data, size); 192 ret = w.buffer; 193 buffersCreated~=w; 194 return ret; 195 } 196 HipAudioBufferAPI* _getBufferAPI(ubyte[] data, uint size) 197 { 198 HipAudioBuffer* temp = new HipAudioBuffer(); 199 *temp = getBuffer(data, size); 200 return cast(HipAudioBufferAPI*)temp; 201 } 202 IHipAudioClip getAudioClipBackend(){return this;} 203 204 package final void setBufferAvailable(HipAudioBuffer buffer) 205 { 206 HipAudioBufferWrapper* w = findBuffer(buffer); 207 ErrorHandler.assertExit(w != null, "AudioClip Error: No buffer was found when trying to set it available"); 208 buffersToRecycle~= *w; 209 w.isAvailable = true; 210 } 211 212 /** 213 * Saves which data should be decoded and do 1 decoding frame 214 */ 215 public uint loadStreamed(in ubyte[] data, HipAudioEncoding encoding) 216 { 217 dataToDecode = cast(ubyte[])data; 218 this.encoding = encoding; 219 ErrorHandler.assertExit(chunkSize > 0, "Can't update stream with 0 sized buffer."); 220 uint dec = decoder.startDecoding(dataToDecode, outBuffer, chunkSize, encoding); 221 totalDecoded+= dec; 222 onUpdateStream(outBuffer, dec); 223 return dec; 224 } 225 226 ///Returns the streambuffer if streamed, else, returns entire sound 227 public ubyte[] getClipData() 228 { 229 if(isStreamed) 230 return outBuffer; 231 return decoder.getClipData(); 232 } 233 ///Returns how much has been decoded 234 public size_t getClipSize() 235 { 236 if(isStreamed) 237 return totalDecoded; 238 return decoder.getClipSize(); 239 } 240 public float getDuration(){return decoder.getDuration();} 241 public final uint getSampleRate(){return decoder.getSamplerate();} 242 public final float getDecodedDuration() 243 { 244 AudioConfig cfg = decoder.getAudioConfig(); 245 return getClipSize() / (cast(float) cfg.sampleRate); 246 } 247 248 override void onFinishLoading(){} 249 override bool isReady() const { return true; } 250 251 override void onDispose() 252 { 253 decoder.dispose(); 254 foreach (ref b; buffersCreated) 255 destroyBuffer(&b.buffer); 256 buffersCreated.length = 0; 257 if(outBuffer != null) 258 { 259 destroy(outBuffer); 260 outBuffer = null; 261 } 262 } 263 } 264 265 266 267 /** 268 * Unpacks the HipAudioBufferAPI into a HipAudioBuffer. 269 * ClipSize with size different than 0 is used for streamed audio 270 */ 271 HipAudioBuffer getBufferFromAPI(IHipAudioClip clip, size_t clipSize = 0) 272 { 273 import hip.util.memory; 274 if(clipSize == 0) 275 clipSize = clip.getClipSize(); 276 HipAudioBufferAPI* api = clip._getBufferAPI(clip.getClipData(), cast(uint)clipSize); 277 HipAudioBuffer buffer = *cast(HipAudioBuffer*)api; 278 freeGCMemory(api); 279 return buffer; 280 }